using System;
using System.IO;
using System.Text;
using ParserGenerator.Extensions;

namespace ParserGenerator.Output
{
    [Serializable]
    public sealed class IndentedWriter : TextWriter
    {
        private const int DefaultTabSize = 4;

        private readonly TextWriter _baseWriter;
        private readonly bool _takeOwn;

        private int _indent;
        private int _tabSize = DefaultTabSize;
        private char _prev = '\n';

        public TextWriter BaseWriter => _baseWriter;
        public override Encoding Encoding => _baseWriter.Encoding;
        public override IFormatProvider FormatProvider => _baseWriter.FormatProvider;

        public int Index { get; private set; }
        public int Line { get; private set; }
        public int Offset { get; private set; }
        public int Column { get; private set; }

        public int Indent
        {
            get { return _indent; }
            set
            {
                if (value < 0)
                    throw new ArgumentOutOfRangeException(nameof(value), SR.IndentMustBeNonNegative);
                _indent = value;
            }
        }

        public int TabSize
        {
            get { return _tabSize; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException(nameof(value), SR.TabSizeMustBePositive);
                _tabSize = value;
            }
        }

        public override string NewLine
        {
            get { return _baseWriter.NewLine; }
            set { _baseWriter.NewLine = value; }
        }

        public IndentedWriter()
            : this(Console.Out)
        {
        }

        public IndentedWriter(TextWriter baseWriter)
            : this(baseWriter, false)
        {
        }

        public IndentedWriter(TextWriter baseWriter, bool takeOwn)
        {
            if (baseWriter == null)
                throw new ArgumentNullException(nameof(baseWriter));
            _baseWriter = baseWriter;
            _takeOwn = takeOwn;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && _takeOwn)
                _baseWriter.Dispose();
            base.Dispose(disposing);
        }

        private void OutputChar(char value)
        {
            if (value.IsLineTerminator())
            {
                if (_prev == '\r' && value == '\n')
                    Index++;
                else
                {
                    _baseWriter.WriteLine();
                    Line++;
                    Offset = 0;
                    Column = 0;
                }
            }
            else
            {
                Offset++;
                if (value == '\t')
                {
                    for (int k = 0; k < _tabSize; k++)
                        _baseWriter.Write(' ');
                    Column = (Column / _tabSize + 1) * _tabSize;
                }
                else
                {
                    _baseWriter.Write(value);
                    Column++;
                }
            }
            Index++;
            _prev = value;
        }

        public override void Write(char value)
        {
            if (_prev.IsLineTerminator() && !value.IsLineTerminator())
                for (int i = 0; i < _indent; i++)
                    OutputChar(' ');
            OutputChar(value);
        }

        public void SpaceTo(int column)
        {
            if (column < 0)
                throw new ArgumentOutOfRangeException(nameof(column), SR.ColumnMustBeNonNegative);
            while (Column < column)
                OutputChar(' ');
        }

        public void OpenBlock() => Indent += TabSize;
        public void CloseBlock() => Indent -= TabSize;

        public void OpenBlock(string delimiter)
        {
            if (delimiter == null)
                throw new ArgumentNullException(nameof(delimiter));
            Write(delimiter);
            Write("\n");
            OpenBlock();
        }

        public void CloseBlock(string delimiter)
        {
            if (delimiter == null)
                throw new ArgumentNullException(nameof(delimiter));
            CloseBlock();
            Write(delimiter);
            Write("\n");
        }

        public void OpenParenBlock() => OpenBlock("(");
        public void CloseParenBlock() => CloseBlock(")");
        public void OpenBracketBlock() => OpenBlock("[");
        public void CloseBracketBlock() => CloseBlock("]");
        public void OpenCurlyBlock() => OpenBlock("{");
        public void CloseCurlyBlock() => CloseBlock("}");
        public void OpenAngleBlock() => OpenBlock("<");
        public void CloseAngleBlock() => CloseBlock(">");

        public IDisposable ColumnAlignment => new WithAlignment(this);

        [Serializable]
        private sealed class WithAlignment : IDisposable
        {
            private readonly IndentedWriter _writer;
            private readonly int _saved;

            public WithAlignment(IndentedWriter writer)
            {
                if (writer == null)
                    throw new ArgumentNullException(nameof(writer));
                _writer = writer;
                _saved = writer.Indent;
                writer.Indent = writer.Column;
            }

            public void Dispose()
            {
                _writer.Indent = _saved;
            }
        }
    }
}
